package algorithm;

import weka.classifiers.Classifier;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;

public class AWE {

	private static final class ClassifierWeightComparator implements
			java.util.Comparator<double[]> {
		@Override
		public int compare(double[] o1, double[] o2) {
			if (o1[0] > o2[0])
				return 1;
			else if (o1[0] < o2[0])
				return -1;
			else
				return 0;
		}
	}

	private double trainingWeightSeenByModel = 0.0; // moa abstract classifier
			
	protected static java.util.Comparator<double[]> weightComparator = new ClassifierWeightComparator();

	protected long[] classDistributions;
	protected Classifier[] ensemble;
	protected Classifier[] pool;
	protected double[] ensembleWeights;
	protected double[][] storedWeights;

//	protected int processedInstances;
//	protected int chunkSize;
//	protected int numFolds;
//	protected int maxMemberCount;
//	protected int maxStoredCount;

	protected Classifier baseLearners;
	protected Instances currentChunk;
	protected Instances dataset; // TODO
	
	public void setLearnModel(Classifier cls){
		this.baseLearners = cls;
	}
	
	public void trainOnInstance(Instance inst) {
		if (inst.weight() > 0.0) {
			this.trainingWeightSeenByModel += inst.weight();
			trainOnInstanceImpl(inst);
		}
	}
	
	public void trainOnInstanceImpl(Instance inst) {
		this.initVariables();

		this.classDistributions[(int) inst.classValue()]++;
		this.currentChunk.add(inst);
		this.processedInstances++;

		if (this.processedInstances % this.chunkSize == 0) {
			this.processChunk();
		}
	}

	private void initVariables() {
		if (this.currentChunk == null) {
			this.currentChunk = new Instances(dataset);
		}

		if (this.classDistributions == null) {
			this.classDistributions = new long[dataset.classAttribute().numValues()];

			for (int i = 0; i < this.classDistributions.length; i++) {
				this.classDistributions[i] = 0;
			}
		}
	}

	/*
	 *   processChunk(){
	 *   
	 *    weight <- cmputeCandidateWeight(this.basedLearner, currentChunk);
	 *    
	 *    each candidate for pool
	 *      candidate->weight = computeWeight(candidate,currentChunk); 
	 *     
	 *    if(!pool.isFull) 
	 *       pool.add(weigth,basedLearner)
	 *       
	 *    else   
	 *       replace the candidate with lowest weight
	 *   }
	 * 
	 *   use the k candidate with highest weight to make an ensemble 
	 * 
	 */
	
	protected void processChunk() {
		// Compute weights
		double candidateClassifierWeight = this.computeCandidateWeight(
				this.baseLearners, this.currentChunk, this.numFolds);

		for (int i = 0; i < this.pool.length; i++) {
			this.storedWeights[i][0] = this.computeWeight(
					this.pool[i], this.currentChunk);
		}

		if (this.pool.length < this.maxStoredCount) {
			// Train and add classifier
			for (int num = 0; num < this.chunkSize; num++) {
				this.baseLearners.trainOnInstance(this.currentChunk
						.instance(num));
			}

			this.addToStored(this.baseLearners,
					candidateClassifierWeight);
		} else {
			// Substitute poorest classifier
			java.util.Arrays.sort(this.storedWeights, weightComparator);

			if (this.storedWeights[0][0] < candidateClassifierWeight) {
				for (int num = 0; num < this.chunkSize; num++) {
					this.baseLearners.trainOnInstance(this.currentChunk
							.instance(num));
				}

				this.storedWeights[0][0] = candidateClassifierWeight;
//				this.storedLearners[(int) this.storedWeights[0][1]] = this.candidateClassifier
//						.copy();
				this.pool[(int) this.storedWeights[0][1]] = Classifier.makeCopy(this
						.baseLearners);
				
			}
		}

		int ensembleSize = java.lang.Math.min(this.pool.length,
				this.maxMemberCount);
		this.ensemble = new Classifier[ensembleSize];
		this.ensembleWeights = new double[ensembleSize];

		// Sort learners according to their weights
		java.util.Arrays.sort(this.storedWeights, weightComparator);

		// Select top k classifiers to construct the ensemble
		for (int i = 0; i < ensembleSize; i++) {
			this.ensembleWeights[i] = this.storedWeights[ensembleSize - i - 1][0];
			this.ensemble[i] = this.pool[(int) this.storedWeights[ensembleSize
					- i - 1][1]];
		}

		this.classDistributions = null;
		this.currentChunk = null;
//		this.candidateClassifier = (Classifier) Class.forName("LibSVM").newInstance();
//		this.candidateClassifier.resetLearning();
	}

	protected double computeCandidateWeight(Classifier candidate,
			Instances chunk, int numFolds) {
		double candidateWeight = 0.0;
		java.util.Random random = new java.util.Random(1);
		Instances randData = new Instances(chunk);
		randData.randomize(random);
		if (randData.classAttribute().isNominal()) {
			randData.stratify(numFolds);
		}

		for (int n = 0; n < numFolds; n++) {
			Instances train = randData.trainCV(numFolds, n, random);
			Instances test = randData.testCV(numFolds, n);

//			Classifier learner = candidate.copy();
			Classifier learner = Classifier.makeCopy(candidate);

			for (int num = 0; num < train.numInstances(); num++) {
				learner.trainOnInstance(train.instance(num));
			}

			candidateWeight += computeWeight(learner, test);
		}

		return candidateWeight / numFolds;
	}

	protected double computeWeight(Classifier learner, Instances chunk) {
		double mse_i = 0;
		double mse_r = 0;

		double f_ci;
		double voteSum;

		for (int i = 0; i < chunk.numInstances(); i++) {
			try {
				voteSum = 0;
				for (double element : learner.getVotesForInstance(chunk
						.instance(i))) {
					voteSum += element;
				}

				if (voteSum > 0) {
					f_ci = learner.getVotesForInstance(chunk.instance(i))[(int) chunk
							.instance(i).classValue()]
							/ voteSum;
					mse_i += (1 - f_ci) * (1 - f_ci);
				} else {
					mse_i += 1;
				}
			} catch (Exception e) {
				mse_i += 1;
			}
		}

		mse_i /= this.chunkSize;
		mse_r = this.computeMseR();

		return java.lang.Math.max(mse_r - mse_i, 0);
	}

	public Classifier[] getSubClassifiers() {
		return this.ensemble.clone();
	}

	protected Classifier addToStored(Classifier newClassifier,
			double newClassifiersWeight) throws Exception {
		Classifier addedClassifier = null;
		Classifier[] newStored = new Classifier[this.pool.length + 1];
		double[][] newStoredWeights = new double[newStored.length][2];

		for (int i = 0; i < newStored.length; i++) {
			if (i < this.pool.length) {
				newStored[i] = this.pool[i];
				newStoredWeights[i][0] = this.storedWeights[i][0];
				newStoredWeights[i][1] = i;
			} else {
//				newStored[i] = addedClassifier = newClassifier.copy();
				newStored[i] = addedClassifier = Classifier.makeCopy(newClassifier);
				newStoredWeights[i][0] = newClassifiersWeight;
				newStoredWeights[i][1] = i;
			}
		}
		this.pool = newStored;
		this.storedWeights = newStoredWeights;

		return addedClassifier;
	}

	 protected double computeMseR()
	         {
	 double p_c;
	 double mse_r = 0;
	 
	 for (int i = 0; i < this.classDistributions.length; i++)
	 {
	 p_c = (double) this.classDistributions[i] / (double) this.chunkSize;
	 mse_r += p_c * ((1 - p_c) * (1 - p_c));
	 }
	 
	 return mse_r;
	 }

	protected int removePoorestModelBytes() {
		int poorestIndex = Utils.minIndex(this.ensembleWeights);
//		int byteSize = this.ensemble[poorestIndex].measureByteSize();
		discardModel(poorestIndex);
//		return byteSize;
		return 0;
	}

	protected void discardModel(int index) {
		Classifier[] newEnsemble = new Classifier[this.ensemble.length - 1];
		double[] newEnsembleWeights = new double[newEnsemble.length];
		int oldPos = 0;
		for (int i = 0; i < newEnsemble.length; i++) {
			if (oldPos == index) {
				oldPos++;
			}
			newEnsemble[i] = this.ensemble[oldPos];
			newEnsembleWeights[i] = this.ensembleWeights[oldPos];
			oldPos++;
		}
		this.ensemble = newEnsemble;
		this.ensembleWeights = newEnsembleWeights;
	}

}
